"""
Custom exception classes for the data preprocessing pipeline.

This module defines a hierarchy of custom exceptions to provide specific error handling
for different types of failures that can occur during preprocessing operations.
"""

from typing import Optional, List, Any


class PreprocessingPipelineError(Exception):
    """
    Base exception class for all preprocessing pipeline errors.
    
    This is the root exception class that all other pipeline-specific exceptions
    inherit from. It provides common functionality for error reporting and logging.
    """
    
    def __init__(self, message: str, details: Optional[str] = None, error_code: Optional[str] = None):
        """
        Initialize the base preprocessing pipeline error.
        
        Args:
            message (str): Human-readable error message
            details (Optional[str]): Additional error details for debugging
            error_code (Optional[str]): Unique error code for programmatic handling
        """
        super().__init__(message)
        self.message = message
        self.details = details
        self.error_code = error_code
    
    def __str__(self) -> str:
        """Return formatted error message."""
        if self.details:
            return f"{self.message} - Details: {self.details}"
        return self.message
    
    def get_user_message(self) -> str:
        """Get user-friendly error message without technical details."""
        return self.message
    
    def get_full_details(self) -> dict:
        """Get complete error information as dictionary."""
        return {
            'error_type': self.__class__.__name__,
            'message': self.message,
            'details': self.details,
            'error_code': self.error_code
        }


class FileOperationError(PreprocessingPipelineError):
    """
    Exception raised for file-related operations errors.
    
    This includes file not found, permission denied, invalid format,
    and other file system related issues.
    """
    
    def __init__(self, message: str, file_path: Optional[str] = None, 
                 operation: Optional[str] = None, details: Optional[str] = None):
        """
        Initialize file operation error.
        
        Args:
            message (str): Error message
            file_path (Optional[str]): Path to the file that caused the error
            operation (Optional[str]): The operation that failed (read, write, validate, etc.)
            details (Optional[str]): Additional error details
        """
        super().__init__(message, details, "FILE_ERROR")
        self.file_path = file_path
        self.operation = operation
    
    def get_user_message(self) -> str:
        """Get user-friendly error message for file operations."""
        if self.file_path:
            return f"File operation failed for '{self.file_path}': {self.message}"
        return f"File operation failed: {self.message}"


class DataValidationError(PreprocessingPipelineError):
    """
    Exception raised for data validation failures.
    
    This includes invalid data structure, missing required columns,
    incorrect data types, and other data quality issues.
    """
    
    def __init__(self, message: str, validation_type: Optional[str] = None,
                 expected_format: Optional[str] = None, actual_format: Optional[str] = None,
                 details: Optional[str] = None):
        """
        Initialize data validation error.
        
        Args:
            message (str): Error message
            validation_type (Optional[str]): Type of validation that failed
            expected_format (Optional[str]): Expected data format
            actual_format (Optional[str]): Actual data format found
            details (Optional[str]): Additional error details
        """
        super().__init__(message, details, "DATA_VALIDATION_ERROR")
        self.validation_type = validation_type
        self.expected_format = expected_format
        self.actual_format = actual_format
    
    def get_user_message(self) -> str:
        """Get user-friendly error message for data validation."""
        if self.expected_format and self.actual_format:
            return f"Data validation failed: Expected {self.expected_format}, but found {self.actual_format}. {self.message}"
        return f"Data validation failed: {self.message}"


class ConfigurationError(PreprocessingPipelineError):
    """
    Exception raised for configuration-related errors.
    
    This includes invalid user input, missing configuration parameters,
    and configuration validation failures.
    """
    
    def __init__(self, message: str, parameter: Optional[str] = None,
                 valid_range: Optional[str] = None, provided_value: Optional[Any] = None,
                 details: Optional[str] = None):
        """
        Initialize configuration error.
        
        Args:
            message (str): Error message
            parameter (Optional[str]): Configuration parameter that caused the error
            valid_range (Optional[str]): Description of valid values
            provided_value (Optional[Any]): The invalid value that was provided
            details (Optional[str]): Additional error details
        """
        super().__init__(message, details, "CONFIG_ERROR")
        self.parameter = parameter
        self.valid_range = valid_range
        self.provided_value = provided_value
    
    def get_user_message(self) -> str:
        """Get user-friendly error message for configuration errors."""
        if self.parameter and self.valid_range:
            return f"Invalid configuration for '{self.parameter}': {self.message}. Valid range: {self.valid_range}"
        return f"Configuration error: {self.message}"


class PreprocessorError(PreprocessingPipelineError):
    """
    Exception raised for preprocessor-related errors.
    
    This includes preprocessor not found, preprocessor execution failures,
    and preprocessor registration issues.
    """
    
    def __init__(self, message: str, processor_id: Optional[int] = None,
                 processor_name: Optional[str] = None, layer_index: Optional[int] = None,
                 details: Optional[str] = None):
        """
        Initialize preprocessor error.
        
        Args:
            message (str): Error message
            processor_id (Optional[int]): ID of the preprocessor that caused the error
            processor_name (Optional[str]): Name of the preprocessor
            layer_index (Optional[int]): Layer index where the error occurred
            details (Optional[str]): Additional error details
        """
        super().__init__(message, details, "PREPROCESSOR_ERROR")
        self.processor_id = processor_id
        self.processor_name = processor_name
        self.layer_index = layer_index
    
    def get_user_message(self) -> str:
        """Get user-friendly error message for preprocessor errors."""
        if self.processor_name and self.layer_index is not None:
            return f"Preprocessor '{self.processor_name}' failed at layer {self.layer_index + 1}: {self.message}"
        elif self.processor_id is not None:
            return f"Preprocessor (ID: {self.processor_id}) failed: {self.message}"
        return f"Preprocessor error: {self.message}"


class ProcessingError(PreprocessingPipelineError):
    """
    Exception raised for processing pipeline execution errors.
    
    This includes combination execution failures, batch processing errors,
    and pipeline orchestration issues.
    """
    
    def __init__(self, message: str, combination_id: Optional[str] = None,
                 combination: Optional[List[int]] = None, stage: Optional[str] = None,
                 details: Optional[str] = None):
        """
        Initialize processing error.
        
        Args:
            message (str): Error message
            combination_id (Optional[str]): ID of the combination being processed
            combination (Optional[List[int]]): The combination that caused the error
            stage (Optional[str]): Processing stage where the error occurred
            details (Optional[str]): Additional error details
        """
        super().__init__(message, details, "PROCESSING_ERROR")
        self.combination_id = combination_id
        self.combination = combination
        self.stage = stage
    
    def get_user_message(self) -> str:
        """Get user-friendly error message for processing errors."""
        if self.combination_id:
            return f"Processing failed for combination '{self.combination_id}': {self.message}"
        return f"Processing error: {self.message}"


class OutputError(PreprocessingPipelineError):
    """
    Exception raised for output generation errors.
    
    This includes file writing failures, directory creation issues,
    and output formatting problems.
    """
    
    def __init__(self, message: str, output_path: Optional[str] = None,
                 operation: Optional[str] = None, details: Optional[str] = None):
        """
        Initialize output error.
        
        Args:
            message (str): Error message
            output_path (Optional[str]): Path where output was being written
            operation (Optional[str]): Output operation that failed
            details (Optional[str]): Additional error details
        """
        super().__init__(message, details, "OUTPUT_ERROR")
        self.output_path = output_path
        self.operation = operation
    
    def get_user_message(self) -> str:
        """Get user-friendly error message for output errors."""
        if self.output_path:
            return f"Output generation failed for '{self.output_path}': {self.message}"
        return f"Output error: {self.message}"


class ResourceError(PreprocessingPipelineError):
    """
    Exception raised for resource-related errors.
    
    This includes memory limitations, disk space issues,
    and other system resource constraints.
    """
    
    def __init__(self, message: str, resource_type: Optional[str] = None,
                 current_usage: Optional[str] = None, limit: Optional[str] = None,
                 details: Optional[str] = None):
        """
        Initialize resource error.
        
        Args:
            message (str): Error message
            resource_type (Optional[str]): Type of resource (memory, disk, etc.)
            current_usage (Optional[str]): Current resource usage
            limit (Optional[str]): Resource limit that was exceeded
            details (Optional[str]): Additional error details
        """
        super().__init__(message, details, "RESOURCE_ERROR")
        self.resource_type = resource_type
        self.current_usage = current_usage
        self.limit = limit
    
    def get_user_message(self) -> str:
        """Get user-friendly error message for resource errors."""
        if self.resource_type and self.limit:
            return f"Resource limit exceeded for {self.resource_type} (limit: {self.limit}): {self.message}"
        return f"Resource error: {self.message}"


class UserCancellationError(PreprocessingPipelineError):
    """
    Exception raised when user cancels an operation.
    
    This is used to distinguish user-initiated cancellations from actual errors.
    """
    
    def __init__(self, message: str = "Operation cancelled by user", stage: Optional[str] = None):
        """
        Initialize user cancellation error.
        
        Args:
            message (str): Cancellation message
            stage (Optional[str]): Stage where cancellation occurred
        """
        super().__init__(message, None, "USER_CANCELLED")
        self.stage = stage
    
    def get_user_message(self) -> str:
        """Get user-friendly cancellation message."""
        if self.stage:
            return f"Operation cancelled during {self.stage}"
        return self.message


# Utility functions for error handling

def handle_file_operation_error(func):
    """
    Decorator to handle file operation errors and convert them to FileOperationError.
    
    Args:
        func: Function to wrap with file error handling
        
    Returns:
        Wrapped function that converts file errors to FileOperationError
    """
    def wrapper(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except FileNotFoundError as e:
            raise FileOperationError(
                "File not found",
                file_path=str(e).split("'")[1] if "'" in str(e) else None,
                operation="access",
                details=str(e)
            )
        except PermissionError as e:
            raise FileOperationError(
                "Permission denied",
                operation="access",
                details=str(e)
            )
        except OSError as e:
            raise FileOperationError(
                "File system error",
                operation="access",
                details=str(e)
            )
        except Exception as e:
            if not isinstance(e, PreprocessingPipelineError):
                raise FileOperationError(
                    "Unexpected file operation error",
                    operation="unknown",
                    details=str(e)
                )
            raise
    
    return wrapper


def validate_input_parameter(value: Any, parameter_name: str, valid_type: type,
                           min_value: Optional[Any] = None, max_value: Optional[Any] = None,
                           valid_values: Optional[List[Any]] = None) -> None:
    """
    Validate input parameter and raise ConfigurationError if invalid.
    
    Args:
        value: Value to validate
        parameter_name (str): Name of the parameter for error reporting
        valid_type (type): Expected type of the value
        min_value (Optional[Any]): Minimum allowed value
        max_value (Optional[Any]): Maximum allowed value
        valid_values (Optional[List[Any]]): List of valid values
        
    Raises:
        ConfigurationError: If validation fails
    """
    # Type validation
    if not isinstance(value, valid_type):
        raise ConfigurationError(
            f"Invalid type for parameter '{parameter_name}'",
            parameter=parameter_name,
            valid_range=f"Expected {valid_type.__name__}",
            provided_value=f"{type(value).__name__}: {value}"
        )
    
    # Range validation
    if min_value is not None and value < min_value:
        raise ConfigurationError(
            f"Value too small for parameter '{parameter_name}'",
            parameter=parameter_name,
            valid_range=f"Minimum: {min_value}",
            provided_value=value
        )
    
    if max_value is not None and value > max_value:
        raise ConfigurationError(
            f"Value too large for parameter '{parameter_name}'",
            parameter=parameter_name,
            valid_range=f"Maximum: {max_value}",
            provided_value=value
        )
    
    # Valid values validation
    if valid_values is not None and value not in valid_values:
        raise ConfigurationError(
            f"Invalid value for parameter '{parameter_name}'",
            parameter=parameter_name,
            valid_range=f"Valid values: {valid_values}",
            provided_value=value
        )


def create_error_summary(errors: List[PreprocessingPipelineError]) -> str:
    """
    Create a summary of multiple errors for user display.
    
    Args:
        errors (List[PreprocessingPipelineError]): List of errors to summarize
        
    Returns:
        str: Formatted error summary
    """
    if not errors:
        return "No errors to report."
    
    summary_lines = [f"Found {len(errors)} error(s):"]
    
    # Group errors by type
    error_groups = {}
    for error in errors:
        error_type = error.__class__.__name__
        if error_type not in error_groups:
            error_groups[error_type] = []
        error_groups[error_type].append(error)
    
    # Format each group
    for error_type, error_list in error_groups.items():
        summary_lines.append(f"\n{error_type} ({len(error_list)} occurrence(s)):")
        for i, error in enumerate(error_list, 1):
            summary_lines.append(f"  {i}. {error.get_user_message()}")
    
    return "\n".join(summary_lines)


# Utility functions for error handling

def create_error_summary(errors: List[Exception]) -> dict:
    """
    Create a summary of multiple errors.
    
    Args:
        errors (List[Exception]): List of exceptions to summarize
        
    Returns:
        dict: Error summary with counts and details
    """
    error_summary = {
        'total_errors': len(errors),
        'error_types': {},
        'error_details': []
    }
    
    for error in errors:
        error_type = type(error).__name__
        error_summary['error_types'][error_type] = error_summary['error_types'].get(error_type, 0) + 1
        
        if isinstance(error, PreprocessingPipelineError):
            error_summary['error_details'].append(error.to_dict())
        else:
            error_summary['error_details'].append({
                'error_type': error_type,
                'message': str(error)
            })
    
    return error_summary


def format_error_for_user(error: Exception) -> str:
    """
    Format an error message for user display.
    
    Args:
        error (Exception): The exception to format
        
    Returns:
        str: User-friendly error message
    """
    if isinstance(error, PreprocessingPipelineError):
        return error.message
    else:
        # For non-custom exceptions, provide a generic message
        error_type = type(error).__name__
        return f"An unexpected {error_type} occurred: {str(error)}"


def is_recoverable_error(error: Exception) -> bool:
    """
    Determine if an error is recoverable.
    
    Args:
        error (Exception): The exception to check
        
    Returns:
        bool: True if the error is recoverable, False otherwise
    """
    # Define recoverable error types
    recoverable_types = (
        UserInputError,
        FileAccessError,
        PreprocessorError,
        FileSaveError
    )
    
    # System errors are generally not recoverable
    non_recoverable_types = (
        MemoryError,
        DiskSpaceError,
        InvalidProcessorError
    )
    
    if isinstance(error, non_recoverable_types):
        return False
    
    if isinstance(error, recoverable_types):
        return True
    
    # For non-custom exceptions, be conservative
    return Falserror_groups[error_type].append(error)
    
    # Format each group
    for error_type, error_list in error_groups.items():
        summary_lines.append(f"\n{error_type} ({len(error_list)} occurrence(s)):")
        for i, error in enumerate(error_list, 1):
            summary_lines.append(f"  {i}. {error.get_user_message()}")
    
    return "\n".join(summary_lines)